﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;


namespace AIthin.Neuro
{
    [Serializable]
    public class Network : INetwork
    {
        /// <summary>
        /// size of input square f.ex. 32x32 inputSize is 32
        /// </summary>
        int inputSize;
        /// <summary>
        /// Lifetime this Network in Cycles or Waves was computetad
        /// </summary>
        public int LifeTime { get; protected set; }
        protected List<int> ComputatedNeurons;
        protected List<int> ActivatedNeurons;
        protected List<EmbryoNeuron> EmbryoNeurons;
        protected Dictionary<int, List<int>> ActivatedNeuronsFrequency;

        public int inputsCount { get; protected set; }
        public int outputsCount { get; protected set; }
        protected int basicNeuronsCount { get; set; }
        public int NeuronsCount { get; protected set; }
        protected static Random random = new Random();
        protected Neuron[] neurons;
        public INeuron this[int index]
        {
            get { return (INeuron)neurons[index]; }
        }

        public delegate void WaveComputed(object sender, string e);
        public event WaveComputed NeuroWaveComputed;

        /// <summary>
        /// dummy Network (must be filled)
        /// </summary>
        public Network() 
        {
        LifeTime = 0;
        ComputatedNeurons = new List<int>();
        ActivatedNeurons = new List<int>();
        ActivatedNeuronsFrequency = new Dictionary<int, List<int>>();
        EmbryoNeurons = new List<EmbryoNeuron>();
        }
        /// <summary>
        /// create network
        /// </summary>
        /// <param name="nc">base neurons count </param>
        /// <param name="inputs">input neurons count</param>
        /// <param name="outputs">outputs neuron count</param>
        public Network(int nc, int inputs, int outputs) : this()
        {
            this.inputsCount = Math.Max(1, inputs);
            this.outputsCount = Math.Max(1, outputs);
            this.basicNeuronsCount = Math.Max(1, nc);
            NeuronsCount = inputsCount + basicNeuronsCount + outputsCount;
            // create collection of neurons
            neurons = new Neuron[NeuronsCount];

            //TODO optimize and precision error?
            double Size = Math.Sqrt((double)inputsCount);

            if (inputsCount != Math.Pow(Size, 2d))
                initInputNeurons();
            else
            {
               inputSize = (int)Size;
               initInputNeurons(inputSize);
            }
 
            initBaseNeurons();

            initOutputNeurons();

         
        }
        private void initOutputNeurons()
        {
            for (int i = inputsCount + basicNeuronsCount; i < inputsCount + basicNeuronsCount + outputsCount; i++)
            {
                // output neurons
                this.neurons[i] = new Neuron(i, 8, 1);
             
                int j = 0;
                int k = 0;
                int neuronToConnect;
                float Xw = 0f, Yw = 0f;
                while (j < neurons[i].InputsCount)
                {
                    //make sure that neuron isnt twice connected at the same neuron
                    neuronToConnect = random.Next(inputsCount, basicNeuronsCount + inputsCount);
                    if (neurons[i].ConnectToOutputOf(ref neurons[neuronToConnect]))
                    {
                        j++;
                        Xw += neurons[neuronToConnect].X;
                        Yw += neurons[neuronToConnect].Y;
                    }
                    else
                        //TODO ??? improove
                        if (++k > 4 * inputsCount)
                            throw new NotImplementedException("no free connection");
                }
                Debug.WriteLine(" output neuron " + neurons[i].ID.ToString() + " connected");
                //set coordinates for the connected neuron.

                if (j > 0)
                {
                    neurons[i].X = Xw / j;
                    neurons[i].Y = Yw / j;
                }
                neurons[i].Z = 32f;
            }
        }
        private void initBaseNeurons()
        {
            //base neurons
            for (int i = inputsCount; i < basicNeuronsCount + inputsCount; i++)
            {
                this.neurons[i] = new Neuron(i, 8, 4);
                int j = 0;
                int k = 0;
                int neuronToConnect;
                float Xw = 0f, Yw = 0f;
                while (j < neurons[i].InputsCount)
                {
                    //TODO make sure that neuron isnt twice connected at the same neuron
                    neuronToConnect = random.Next(0, inputsCount);

                    if (neurons[i].ConnectToOutputOf(ref neurons[neuronToConnect]))
                    {
                        // success
                        j++;
                        Xw += neurons[neuronToConnect].X;
                        Yw += neurons[neuronToConnect].Y;
                    }
                    else
                        //TODO ??? improove
                        if (++k > 4 * inputsCount)
                            throw new NotImplementedException("no free axons connection");
                }

                if (j > 0)
                {
                    neurons[i].X = Xw / j;
                    neurons[i].Y = Yw / j;
                }
                neurons[i].Z = 16f;

                Debug.WriteLine(" neuron " + neurons[i].ID.ToString() + " connected");
            }
        }
        private void initInputNeurons()
        {
            // The first neurons at Input, amount -> InputsCount
            for (int i = 0; i < inputsCount; i++)
            {
                this.neurons[i] = new Neuron(i, 1, 8);
                // set positions
                //TODO optimize and precision error?
                double Size = Math.Sqrt((double)inputsCount);

                if (Size != Math.Pow((double)inputsCount, 2d))
                {
                    neurons[i].X = (float)(random.NextDouble() * 2d);
                    neurons[i].Y = (float)(random.NextDouble() * 2d);
                }
                else
                {
                    neurons[i].X = (float)(random.NextDouble() * 2d);
                    neurons[i].Y = (float)(random.NextDouble() * 2d);
                }

                neurons[i].Z = 0f;
                Debug.WriteLine(neurons[i].ID, "created input neuron");
            }
        }
        /// <summary>
        /// create Square input field
        /// </summary>
        /// <param name="Size">nuumber neurons at side</param>
        private void initInputNeurons(int Size)
        {
            // raster
            float Step = 1.0f;
            // The first neurons at Input, amount -> InputsCount
            int k = 0;
            for (int j = 0; j < Size; j++)
            {
                for (int i = 0; i < Size; i++)
                {
                    this.neurons[k] = new Neuron(k, 1, 4);
                    // set positions
                    neurons[k].X = (float)(i * Step);
                    neurons[k].Y = (float)(j * Step);
                    neurons[k].Z = 0f;
                    Debug.WriteLine(neurons[k].ID, "created input neuron");
                    // next neuron
                    k++;
                }
            }
        }
        /// <summary>
        /// Sets Input Signal to inputs neurons
        /// </summary>
        /// <param name="input">1-black, 0-white </param>
        public void SetInputSignal(ref short[] input)
        {
            for (int i = 0; i < inputsCount; i++)
            {
                //TODO change input sheme
                // ! Input index equal with the neuron number
                neurons[i].SetInput(input[i]);
            }
        }
        public void SetInputSignal(ref short[,] input)
        {
            int k = 0;
            for (int i = 0; i < inputSize; i++)
                for (int j = 0; j < inputSize; j++)
                {
                    //TODO change input sheme
                    // ! Input index equal with the neuron number

                    //Debug.Write(k, "neuron");
                    neurons[k++].SetInput(input[i, j]);
                    //Debug.WriteLine(input[i, j], " set input");
                }
        }
        /// <summary>
        /// set output signal for each outputs(end) neurons
        /// </summary>
        /// <param name="output">value of output 1 to 256</param>
        //TODO output value must be single
        public void SetOutputSignal(ref short[] output)
        {
            for (int i = 0; i < outputsCount; i++)
            {
                //TODO change input sheme
                // ! Input index equal with the neuron number
                //Debug.WriteLine(inputsCount + basicNeuronsCount + i, "set output");
                neurons[inputsCount + basicNeuronsCount + i].SetOutput(output[i]);
            }
        }
        /// <summary>
        /// compue neurons state for one neurons activity wave
        /// </summary>
        /// <param name="input">input matrix 0-black, 1-white</param>
        /// <param name="output">output vektor that accord to input matrix</param>
        public void Tick(ref short[,] input, ref short[] output)
        {
            LifeTime++;
            // TODO reset activation state or compute to another state: low
            Reset();
            // set input signal
            SetInputSignal(ref input);
            // set output (activated output neurons)
            //TODO learn change
  //          SetOutputSignal(ref output);
            // compute neurons state
            // activation welle 
            Compute();
            // learn 
            // make connections -> new neurons -> new axons connect to dendrites on activ neurons
            Connect();
        }
        public void Tick(ref short[,] input)
        {
            LifeTime++;
            // TODO reset activation state or compute to another state: low
            Reset();
            // set input signal
            SetInputSignal(ref input);
            // set output (activated output neurons)
            // compute neurons state
            // activation welle 
            Compute();
            // learn 
            // make connections -> new neurons -> new axons connect to dendrites on activ neurons
            Connect();
        }

       /// <summary>
       /// compute network state for input to output relation
       /// </summary>
        private void Compute()
        {
            // activation wave
            // computate so many times that all of neurons will be computated
            // computation wave can be not sequent
            ComputatedNeurons.Clear();
            ActivatedNeurons.Clear();
            for (int iWave = 0; ComputatedNeurons.Count < NeuronsCount; iWave++)
            {
                ActivationWave(iWave);
                Debug.WriteLine(iWave, " wave");
                Debug.WriteLine(ComputatedNeurons.Count, " computated neurons");
                Debug.WriteLine(NeuronsCount, " from");

            }
        }
        /// <summary>
        /// compute activation wave. It will be all neurons computated thay are have 
        /// </summary>
        /// <param name="iWave">wave count for this tick</param>
        private void ActivationWave(int iWave)
        {
            foreach (Neuron neuron in neurons)
            {
                if (neuron.Computate())
                {
                    ComputatedNeurons.Add(neuron.ID);
                    if (neuron.Activated)
                    {
                        ActivatedNeurons.Add(neuron.ID);
                    }

                    //Debug.WriteLine(neuron.ID, " computated neuron");
                    //signal weitergeben
                    foreach (AxonTerminal a in neuron.outputs) neurons[a.ConnectedNeuron].ActivateInput(a.ConnectedDendrit, a.Signal);
                }
                //TODO event for wave
            //    NeuroWaveComputed(this, "wave " + iWave.ToString() + " computed");
            }
        }
        /// <summary>
        /// make new neurons and connections (see Embryos)
        /// </summary>
        private void Connect()
        {
            //TODO learn -> make connections, correct threshould and weights
            // ActivityMemorize();
            //check embryos to be born
            CheckEmbryos(ActivatedNeurons);
            //let's embryos born
            foreach (EmbryoNeuron i in EmbryoNeurons)
            {
                //RES speed opt
                if (i.ReadyToBorn & !i.wasBorn)
                {
                    //TODO 1.5f 
                    //TODO new neuron must be connected!!
                    this.AddNeuron(i.Connections, new Neuron(this.NeuronsCount,i.Connections.Length,1), 1.5f);
                    i.wasBorn = true;
                    Debug.WriteLine(i.ID,  "embryo was born");

                    //TODO at first dont remove embryos, but later must to remove they
                    //I shell use output have i ones or not (active neurons with free outputs) ???
                }
            }
        }
        /// <summary>
        /// check the embryos list to make new neurons from embryos
        /// </summary>
        /// <param name="activatedNeurons">list of activated neurons</param>
        private void CheckEmbryos(List<int> activatedNeurons)
        {
            if (EmbryoNeurons.Count == 0)
                //while there are any activated neurons (just for first call) in a tick or wave
                while (activatedNeurons.Count > 0)
                    //TODO create embryo just from nearest neurons
                    EmbryoNeurons.Add(new EmbryoNeuron(ref activatedNeurons));
            else
            {
                foreach (EmbryoNeuron i in EmbryoNeurons)
                {
                    //TODO must redefine function IsIn dependet from coordinates
                    //check if activated neurons are a part of existing embryos ,
                    if (i.IsIn(activatedNeurons))
                    {
                        //if yes embryo must grow
                        i.Grow();
                        //remove embryo from list of actives (spin) neurons for this tick
                        //TODO active neurons give signal just for one embryo?
                        i.RemoveFrom(ref activatedNeurons);
                    }
                }
                //create new embryos
                //TODO condition  that in Embryo must have more than 1 neurons is natural
                //RES speed opt
                while (activatedNeurons.Count > 1)
                {
                    EmbryoNeurons.Add(new EmbryoNeuron(ref activatedNeurons));
                }
            }
            return;
        }
        /// <summary>
        /// save all activity of neurons during lifetime
        /// </summary>
        private void ActivityMemorize()
        {
            foreach (int i in ActivatedNeurons)
            {
                //RES optimize speed
                if (ActivatedNeuronsFrequency.ContainsKey(i))
                {
                    List<int> ActivityList = new List<int>();
                    ActivatedNeuronsFrequency.TryGetValue(i, out ActivityList);
                    ActivatedNeuronsFrequency.Remove(i);
                    ActivityList.Add(LifeTime);
                    ActivatedNeuronsFrequency.Add(i, ActivityList);
                }
                else
                {
                    List<int> ActivityList = new List<int>();
                    ActivityList.Add(LifeTime);
                    ActivatedNeuronsFrequency.Add(i, ActivityList);
                }
            
            }
        }
        /// <summary>
        /// reset network for this tick
        /// </summary>
        private void Reset()
        {
            //clear list of computated neurons 
            ComputatedNeurons.Clear();
            ActivatedNeurons.Clear();
            //TODO ActivatedNeuronsFrequency -> clear connected neurons
            //TODO That is false! Neurons must go out naturally. Make a subprog EndComputate();
            foreach (Neuron neuron in neurons) neuron.Reset();
        }
        /// <summary>
        /// add new neuron to network
        /// </summary>
        /// <param name="inputConnections">index of neurons connected for input </param>
        /// <param name="neuron">new neuron</param>
        /// <param name="zOffset">distanse of z-achse for the new neuron</param>
        /// <returns>neurons ID number</returns>
        int AddNeuron(int[] inputConnections, Neuron neuron, float zOffset)
        {
            NeuronsCount++;
            Neuron[] newNeuronsArray = new Neuron[NeuronsCount];
            //RES very slow
            neurons.CopyTo(newNeuronsArray,0);
            neurons = newNeuronsArray;
            neurons[NeuronsCount - 1] = neuron;
            float Xc=0f, Yc=0f, Zc=0f;
            foreach (int i in inputConnections)
            {
                if (!neuron.ConnectToOutputOf(ref neurons[i]))
                {
                    neurons[i].AddOutput();
                    if (!neuron.ConnectToOutputOf(ref neurons[i]))
                        Debug.WriteLine(neuron.ID, "can't connect neuron");
                }
                Xc += neurons[i].X;
                Yc += neurons[i].Y;
                Zc += neurons[i].Z;
            }

            neuron.X = Xc / (float)inputConnections.Length;
            neuron.Y = Yc / (float)inputConnections.Length;
            neuron.Z = Zc / (float)inputConnections.Length + zOffset;

            Debug.Write(neuron.ID, "neuron added to network");
            Debug.WriteLine(NeuronsCount, " neurons count");
            return neuron.ID;
        }
        /// <summary>
        /// Save network to specified file.
        /// </summary>
        /// 
        /// <param name="fileName">File name to save network into.</param>
        /// 
        /// <remarks><para>The neural network is saved using .NET serialization (binary formatter is used).</para></remarks>
        /// 
        public void Save(string fileName)
        {
            FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
            Save(stream);
            stream.Close();
        }
        /// <summary>
        /// Save network to specified file.
        /// </summary>
        /// <param name="stream">Stream to save network into.</param>
        /// <remarks><para>The neural network is saved using .NET serialization (binary formatter is used).</para></remarks>
        public void Save(Stream stream)
        {
            IFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, this);
        }
        /// <summary>
        /// Load network from specified file.
        /// </summary>
        /// <param name="fileName">File name to load network from.</param>
        /// <returns>Returns instance of <see cref="Network"/> class with all properties initialized from file.</returns>
        /// <remarks><para>Neural network is loaded from file using .NET serialization (binary formater is used).</para></remarks>
        public INetwork Load(string fileName)
        {
            FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);

            Network network = new Network();
            network = Load(stream);
            stream.Close();

            return network;
        }
        /// <summary>
        /// Load network from specified file.
        /// </summary>
        /// <param name="stream">Stream to load network from.</param>
        /// <returns>Returns instance of <see cref="Network"/> class with all properties initialized from file.</returns>
        /// <remarks><para>Neural network is loaded from file using .NET serialization (binary formater is used).</para></remarks>
        public static Network Load(Stream stream)
        {
            IFormatter formatter = new BinaryFormatter();
            Network network = (Network)formatter.Deserialize(stream);
            return network;
        }
   
    }

}
